05-数据存储与Model层开发
# 第5章 数据存储与Model层开发
## 5.1 数据存储方式选择
Xiuno BBS提供5种数据存储方式,根据场景选择:
| 存储方式 | 持久化 | 缓存加速 | 适用场景 | 典型用途 |
| -------- | --- | ---- | ---------- | ----------------------------------- |
| Setting | 是 | 是 | 插件配置项 | `setting_set('my_plugin', $config)` |
| KV | 是 | 否 | 通用键值存储 | `kv_set('key', $value)` |
| KV+Cache | 是 | 是 | 频繁读取的持久数据 | `kv_cache_set('key', $value)` |
| Cache | 否 | 是 | 临时数据/计算结果 | `cache_set('key', $data, 3600)` |
| 数据库表 | 是 | 手动 | 结构化数据/关联查询 | 自建表 + Model函数 |
| Runtime | 是 | 是 | 全站统计信息 | `runtime_set('users+', 1)` |
### 选择决策树
```
需要存储数据?
├─ 是配置项? → Setting
├─ 是简单键值? → KV(不频繁读取)或 KV+Cache(频繁读取)
├─ 是临时数据? → Cache(设过期时间)
├─ 需要复杂查询/关联? → 数据库表
└─ 是统计计数? → Runtime
```
## 5.2 Setting 存储
专用于插件配置,数据存储在`bbs_kv`表中,键名为插件目录名。
```php
// 写入配置
setting_set('my_plugin', array(
'option1' => 'value1',
'option2' => 100,
'option3' => true,
));
// 读取配置
$config = setting_get('my_plugin');
$option1 = $config['option1'];
// 更新部分配置(需先读取再合并)
$config = setting_get('my_plugin');
$config['option1'] = 'new_value';
setting_set('my_plugin', $config);
// 删除配置
setting_delete('my_plugin');
```
> **坑**: `setting_set()`是全量覆盖,不是增量合并。更新单个配置项必须先读取全部配置,修改后再写回。
## 5.3 KV 存储
通用键值存储,数据持久化到`bbs_kv`表。
```php
kv_set('my_key', $value); // 设置
kv_get('my_key'); // 获取(带静态缓存,同请求内只查一次数据库)
kv_delete('my_key'); // 删除
kv_cache_set('my_key', $value); // 设置(同时写入缓存)
kv_cache_get('my_key'); // 获取(优先从缓存读取)
kv_cache_delete('my_key'); // 删除(同时删除缓存)
```
## 5.4 Cache 存储
临时缓存,支持Redis/Memcached/MySQL/APC/YAC多种后端。
```php
cache_set('my_cache_key', $data, 3600); // 缓存1小时
cache_get('my_cache_key'); // 获取(失败返回FALSE)
cache_delete('my_cache_key'); // 删除
cache_truncate(); // 清空所有缓存
```
> **注意**: 缓存键名超过32字符时自动md5加密,所有键添加配置前缀避免冲突。
## 5.5 数据库表设计
### 5.5.1 建表规范
```sql
CREATE TABLE IF NOT EXISTS {表前缀}my_table (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`uid` int(11) unsigned NOT NULL default '0',
`tid` int(11) unsigned NOT NULL default '0',
`message` longtext NOT NULL,
`create_date` int(10) unsigned NOT NULL default '0',
`create_ip` int(10) unsigned NOT NULL default '0',
PRIMARY KEY (id),
KEY (uid),
KEY (tid),
UNIQUE KEY (tid, uid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```
**规范要点**:
- 使用`IF NOT EXISTS`避免重复建表报错
- 主键使用自增`id`
- 时间字段用`int(10)`存储Unix时间戳
- IP地址用`int(10)`存储(`ip2long()`转换)
- 合理添加索引,但不要过度索引
- 字符集统一`utf8`(非utf8mb4,Xiuno核心表使用utf8)
### 5.5.2 全文搜索表
MySQL FULLTEXT索引要求使用MyISAM引擎:
```sql
CREATE TABLE IF NOT EXISTS {表前缀}my_search (
`tid` int(11) unsigned NOT NULL default '0',
`message` longtext NOT NULL,
UNIQUE KEY (tid),
FULLTEXT(message)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
```
### 5.5.3 ALTER TABLE 添加字段
给核心表添加字段是常见需求:
```sql
ALTER TABLE {表前缀}thread ADD COLUMN my_field int(11) DEFAULT '0';
ALTER TABLE {表前缀}user ADD COLUMN my_field int(11) DEFAULT '0';
```
> **坑**:
>
> - 添加字段前必须检查是否已存在(升级场景)
> - 删除字段时同样需要检查
> - 大表ALTER TABLE可能耗时很长,建议在低峰期执行
## 5.6 Model层开发
### 5.6.1 Model文件注册
通过`hook/model_inc_file.php`将Model文件注入到系统加载列表:
```php
**关键**: 末尾必须有逗号!因为这行会被插入到`model.inc.php`的`$include_model_files`数组中。
也可以使用`hook/model_inc_end.php`在Model加载完毕后注入:
```php
$id), $arr);
}
function my_table__read($id) {
return db_find_one('my_table', array('id'=>$id));
}
function my_table__delete($id) {
return db_delete('my_table', array('id'=>$id));
}
function my_table__find($cond = array(), $orderby = array(), $page = 1, $pagesize = 20) {
return db_find('my_table', $cond, $orderby, $page, $pagesize);
}
function my_table_create($arr) {
$r = my_table__create($arr);
cache_delete('my_table');
return $r;
}
function my_table_read($id) {
$data = my_table__read($id);
my_table_format($data);
return $data;
}
function my_table_read_cache($id) {
$key = 'my_table_' . $id;
$data = cache_get($key);
if ($data === NULL) {
$data = my_table_read($id);
cache_set($key, $data, 3600);
}
return $data;
}
function my_table_update($id, $arr) {
$r = my_table__update($id, $arr);
cache_delete('my_table_' . $id);
return $r;
}
function my_table_delete($id) {
$r = my_table__delete($id);
cache_delete('my_table_' . $id);
return $r;
}
function my_table_find($cond = array(), $orderby = array('id'=>-1), $page = 1, $pagesize = 20) {
$datalist = my_table__find($cond, $orderby, $page, $pagesize);
if ($datalist) foreach ($datalist as &$data) {
my_table_format($data);
}
return $datalist;
}
function my_table_format(&$data) {
if (empty($data)) return;
$data['create_date_fmt'] = date('Y-m-d H:i', $data['create_date']);
$data['user'] = user_read_cache($data['uid']);
}
function my_table_count($cond = array()) {
return db_count('my_table', $cond);
}
function my_table_maxid() {
return db_maxid('my_table');
}
```
### 5.6.4 核心数据库操作函数
| 函数 | 用途 | 示例 |
| ---------------------------------------------------- | ------ | ----------------------------------------------------------- |
| `db_create($table, $arr)` | 插入记录 | `db_create('my_table', array('uid'=>1, 'tid'=>2))` |
| `db_update($table, $cond, $arr)` | 更新记录 | `db_update('my_table', array('id'=>1), array('field+'=>1))` |
| `db_delete($table, $cond)` | 删除记录 | `db_delete('my_table', array('id'=>1))` |
| `db_find_one($table, $cond)` | 查询单条 | `db_find_one('my_table', array('id'=>1))` |
| `db_find($table, $cond, $orderby, $page, $pagesize)` | 查询列表 | `db_find('my_table', array(), array('id'=>-1), 1, 20)` |
| `db_count($table, $cond)` | 统计数量 | `db_count('my_table', array('uid'=>1))` |
| `db_maxid($table)` | 获取最大ID | `db_maxid('my_table')` |
| `db_exec($sql)` | 执行SQL | `db_exec("CREATE TABLE ...")` |
| `db_find_index($table)` | 获取表结构 | `db_find_index('my_table')` |
### 5.6.5 增量更新技巧
`db_update()`支持增量更新,键名以`+`/`-`结尾时生成`field=field+1`语法:
```php
// 积分+1
db_update('user', array('uid'=>$uid), array('credits+'=>1));
// 积分-5
db_update('user', array('uid'=>$uid), array('credits-'=>5));
// 浏览数+1
thread_inc_views($tid, 1);
```
### 5.6.6 条件查询语法
```php
// 等值匹配
db_find('my_table', array('uid'=>1));
// 多值OR展开(非IN)
db_find('my_table', array('uid'=>array(1, 2, 3)));
// 生成: WHERE uid=1 OR uid=2 OR uid=3
// 范围匹配
db_find('my_table', array('create_date'=>array('>'=>time()-86400)));
db_find('my_table', array('credits'=>array('>='=>100)));
// LIKE模糊匹配
db_find('my_table', array('message'=>array('LIKE'=>'%keyword%')));
```
> **坑**: 多值条件使用OR展开而非IN,当值很多时SQL性能较差。建议改用`db_exec()`手动编写IN查询。
## 5.7 与核心Model的交互
### 5.7.1 读取核心数据
```php
$user = user_read($uid); // 读取用户
$user = user_read_cache($uid); // 缓存读取用户
$thread = thread_read($tid); // 读取主题
$thread = thread_read_cache($tid); // 缓存读取主题
$post = post_read($pid); // 读取帖子
$forum = forum_read($fid); // 读取版块
$group = group_read($gid); // 读取用户组
```
### 5.7.2 更新核心数据
```php
user_update($uid, array('credits+'=>10)); // 用户积分+10
thread_update($tid, array('views+'=>1)); // 浏览数+1
forum_update($fid, array('todayposts+'=>1)); // 今日发帖+1
```
### 5.7.3 通过Hook扩展核心Model
在核心Model函数的末尾注入自定义逻辑:
```php
// hook/model_thread_create_end.php
// 在thread_create()函数末尾执行
// 可以访问到 $tid, $arr 等变量
db_create('my_table', array('tid'=>$tid, 'uid'=>$uid));
```
**可用的Model层Hook点**:包括但不限于
| Hook点 | 触发时机 | 可用变量 |
| ------------------------------- | ----- | ---------- |
| `model_thread_create_end.php` | 主题创建后 | $tid, $arr |
| `model_thread_delete_start.php` | 主题删除前 | $tid |
| `model_thread_delete_end.php` | 主题删除后 | $tid |
| `model_post_create_end.php` | 帖子创建后 | $pid, $arr |
| `model_post_delete_start.php` | 帖子删除前 | $pid |
| `model_post_update_end.php` | 帖子更新后 | $pid, $arr |
| `model_user_create_end.php` | 用户创建后 | $uid, $arr |
| `model_user_delete_start.php` | 用户删除前 | $uid |
| `model_forum_update_end.php` | 版块更新后 | $fid, $arr |